﻿using System;
using System.Linq;
using System.Reflection;

namespace NthDownload.Common.Mapping
{
    public class ObjectMappingService
    {
        #region Static Methods

        /// <summary>
        /// Returns a new instance of the SourceObject with the values of any properties marked with the [MappableProperty] attribute 
        /// copied from the SourceObject. Note if a property who's type is not a primitive is marked with the [MappableProperty] attribute,  
        /// a clone will be made using a nest call this this Clone method unless that object implementes IClonable.
        /// </summary>
        /// <param name="sourceObject">Object to get values from</param>
        /// <returns>New Instance</returns>
        public static object Clone(object sourceObject)
        {
            //
            // Check for null
            //
            if (sourceObject == null)
            {
                return null;
            }

            //
            // If object implements ICloneable then clone like that instead of copying it's properties
            //
            if (sourceObject is ICloneable)
            {
                return ((ICloneable)sourceObject).Clone();
            }

            //
            // Create new instance
            //
            object newInstance = Activator.CreateInstance(sourceObject.GetType());

            //
            // Get all properties for SourceObject.
            //
            PropertyInfo[] properties = sourceObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

            //
            // If there are sub properties then traverse properties,
            // If no sub properties then object is like a decimal that is not primitive, then
            // simply match the value of the object
            //
            if (properties.Any())
            {
                foreach (PropertyInfo p in properties)
                {
                    //
                    // Check current property for the MappableAttribute attribute
                    //
                    MappableProperty[] customAttributes = (MappableProperty[])p.GetCustomAttributes(typeof(MappableProperty), true);
                    if (customAttributes.Length > 0)
                    {
                        if (p.PropertyType.IsPrimitive)
                        {
                            p.SetValue(newInstance, p.GetValue(sourceObject, null), null);
                        }
                        else 
                        {
                            if (customAttributes[0].ReferenceClone)
                            {
                                p.SetValue(newInstance, p.GetValue(sourceObject, null), null);
                            }
                            else
                            {

                                MappableObject[] mappableObjectAttributes = new MappableObject[0];
                                object propertyValue = p.GetValue(sourceObject, null);
                                if (propertyValue != null)
                                {
                                    mappableObjectAttributes = (MappableObject[])propertyValue.GetType().GetCustomAttributes(typeof(MappableObject), true);
                                }

                                if (mappableObjectAttributes.Length > 0)
                                {
                                    p.SetValue(newInstance, Clone(sourceObject), null);
                                }
                                else if (p.GetValue(sourceObject, null) is ICloneable)
                                {
                                    p.SetValue(newInstance, ((ICloneable)p.GetValue(sourceObject, null)).Clone(), null);
                                }
                                else
                                {
                                    p.SetValue(newInstance, p.GetValue(sourceObject, null), null);
                                }                                
                            }
                        }
                    }
                }
            }
            else
            {
                newInstance = sourceObject;
            }

            return newInstance;
        }

        /// <summary>
        /// Updates the properties marked with the [MappableProperty] attribute in the DestinationObject from the properties of the SourceObject.
        /// </summary>
        /// <param name="sourceObject">Object to get values from</param>
        /// <param name="destinationObject">Object to set values to</param>
        public static void Update(object sourceObject, object destinationObject)
        {
            //
            // Check for null
            //
            if (sourceObject == null)
            {
                return;
            }

            //
            // Get all properties for SourceObject.
            //
            PropertyInfo[] properties = sourceObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

            //
            // Traverse properties
            //
            foreach (PropertyInfo p in properties)
            {
                //
                // Check if property is decorated with the MappableAttribute attribute
                //
                MappableProperty[] mappablePropertyAttributes = (MappableProperty[])p.GetCustomAttributes(typeof(MappableProperty), true);

                if (mappablePropertyAttributes.Length > 0)
                {
                    if (p.PropertyType.IsPrimitive)
                    {
                        p.SetValue(destinationObject, p.GetValue(sourceObject, null), null);
                    }
                    else
                    {
                        if (mappablePropertyAttributes[0].ReferenceClone)
                        {
                            p.SetValue(destinationObject, p.GetValue(sourceObject, null), null);
                        }
                        else
                        {
                            MappableObject[] mappableObjectAttributes = new MappableObject[0];
                            object propertyValue = p.GetValue(sourceObject, null);
                            if (propertyValue != null)
                            {
                                mappableObjectAttributes = (MappableObject[])propertyValue.GetType().GetCustomAttributes(typeof(MappableObject), true);
                            }

                            if (mappableObjectAttributes.Length > 0)
                            {
                                Update(p.GetValue(sourceObject, null), p.GetValue(destinationObject, null));
                                //p.SetValue(DestinationObject, ObjectMappingService.Clone(p.GetValue(SourceObject, null)), null);
                            }
                            else if (p.GetValue(sourceObject, null) is ICloneable)
                            {
                                p.SetValue(destinationObject, ((ICloneable)p.GetValue(sourceObject, null)).Clone(), null);
                            }
                            else
                            {
                                p.SetValue(destinationObject, p.GetValue(sourceObject, null), null);
                            }                            
                        }
                    }
                }
            }

            //
            // Get all methods for destination object
            //
            MethodInfo[] methods = destinationObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance);

            //
            // Traverse methods
            //
            foreach (MethodInfo method in methods)
            {
                //
                // Check if method is decorated with the InvokeAfterUpdate attribute
                //
                InvokeAfterUpdate[] invokeAfterUpdateAttributes = (InvokeAfterUpdate[])method.GetCustomAttributes(typeof(InvokeAfterUpdate), true);

                if (invokeAfterUpdateAttributes.Length > 0 && method.GetParameters().Length == 0)
                {
                    method.Invoke(sourceObject, null);
                }
            }
        }

        #endregion Static Methods
    }
}
